// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package conf

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"time"

	"chainmaker.org/chainmaker/smarteditor/cache"
	"chainmaker.org/chainmaker/smarteditor/db"
	"github.com/jinzhu/gorm"
)

const (
	sizeMax    = "max"
	sizeMin    = "min"
	sizeNormal = "normal"
)

var (
	DefaultLayout = Layout{
		Side:      &Panel{Size: 295, State: sizeNormal},
		SideRight: &Panel{Size: 250, State: sizeMin},
		Bottom:    &Panel{Size: 100, State: sizeNormal},
	}
)

// Panel represents a UI panel.
type Panel struct {
	State string `json:"state"` // panel state, "min"/"max"/"normal"
	Size  uint16 `json:"size"`  // panel size
}

// Layout represents the UI layout.
type Layout struct {
	Side      *Panel `json:"side"`      // Side panel
	SideRight *Panel `json:"sideRight"` // Right-Side panel
	Bottom    *Panel `json:"bottom"`    // Bottom panel
}

// LatestSessionContent represents the latest session content.
type LatestSessionContent struct {
	FileTree    []string `json:"fileTree"`    // paths of expanding nodes of file tree
	Files       []string `json:"files"`       // paths of files of opening editor tabs
	CurrentFile string   `json:"currentFile"` // path of file of the current focused editor tab
	Layout      *Layout  `json:"layout"`      // UI Layout
}

// User configuration.
type User struct {
	UID                   int64  `gorm:"primary_key;AUTO_INCREMENT"` // 数据库中自增主键,唯一id
	ID                    string `gorm:"index:idx_id"`
	Name                  string `gorm:"index:idx_name"`
	Avatar                string
	Workspace             string `gorm:"size:1024"` // the GOPATH of this user (maybe contain several paths splitted by os.PathListSeparator)
	Locale                string
	GoFormat              string
	GoBuildArgsForLinux   string
	GoBuildArgsForWindows string
	GoBuildArgsForDarwin  string
	FontFamily            string
	FontSize              string
	Theme                 string
	Keymap                string                // wide/vim
	Created               int64                 // user create time in unix nano
	Updated               int64                 // preference update time in unix nano
	Lived                 int64                 // the latest session activity in unix nano
	Editor                *editor               `gorm:"-"`
	EditorJson            string                `gorm:"type:text"`
	LatestSessionContent  *LatestSessionContent `gorm:"-"`
	ContentJson           string                `gorm:"type:mediumtext"`
}

func (*User) TableName() string {
	return "users"
}

// Editor configuration of a user.
type editor struct {
	FontFamily string `json:"fontFamily"`
	FontSize   string `json:"fontSize"`
	LineHeight string `json:"lineHeight"`
	Theme      string `json:"theme"`
	TabSize    string `json:"tabSize"`
}

func (u *User) Insert() bool {
	editorBytes, _ := json.Marshal(u.Editor)
	contentBytes, _ := json.Marshal(u.LatestSessionContent)
	u.EditorJson = string(editorBytes)
	u.ContentJson = string(contentBytes)
	err := db.GlobalDB.Create(u).Error
	if err != nil {
		log.Errorf("user %v Insert db got error: %s", *u, err.Error())
		return false
	}
	cache.GlobalUserCache.Add(u.ID, u)
	return true
}

// 更新所有信息
func (u *User) UpdateAll() bool {
	editorBytes, _ := json.Marshal(u.Editor)
	contentBytes, _ := json.Marshal(u.LatestSessionContent)
	u.EditorJson = string(editorBytes)
	u.ContentJson = string(contentBytes)
	err := db.GlobalDB.Save(u).Error
	if err != nil {
		log.Errorf("UpdateAll user %v update db got error: %s", *u, err.Error())
		return false
	}
	cache.GlobalUserCache.Remove(u.ID)
	return true
}

// // 更新选定字段
// func (u *User) UpdateAttributes(attributes map[string]interface{}) bool {
// 	err := db.GlobalDB.Model(u).Updates(attributes).Error
// 	if err != nil {
// 		log.Errorf("user %v update attributes %v got error %s", *u, attributes, err.Error())
// 		return false
// 	}
// 	cache.GlobalUserCache.Add(u.ID, u)
// 	return true
// }

func UpdateUserLastContent(uid string, content *LatestSessionContent) {
	contentBytes, _ := json.Marshal(content)
	err := db.GlobalDB.Model(&User{}).Where("id = ? ", uid).Update("content_json", string(contentBytes)).Error
	if err != nil {
		log.Errorf("UpdateUserLastContent uid [%s] content [%+v] failed [%s]", uid, *content, err.Error())
		return
	}
	cache.GlobalUserCache.Remove(uid)
}

func GetUserByNameFromDB(name string) *User {
	var ret User
	queryErr := db.GlobalDB.Where("name = ?", name).First(&ret).Error
	if queryErr != nil {
		if queryErr != gorm.ErrRecordNotFound {
			log.Errorf("GetUserByNameFromDB %s got error %s", name, queryErr.Error())
		}
		return nil
	}
	var dbEditor editor
	var dbContent LatestSessionContent
	contentErr := json.Unmarshal([]byte(ret.ContentJson), &dbContent)
	editorErr := json.Unmarshal([]byte(ret.EditorJson), &dbEditor)
	if contentErr != nil || editorErr != nil {
		log.Errorf("GetUserByNameFromDB %s unmarshal got contenterr %v,editorErr %v ", name, contentErr, editorErr)
	}
	ret.Editor = &dbEditor
	ret.LatestSessionContent = &dbContent
	cache.GlobalUserCache.Add(ret.ID, &ret)
	return &ret
}

func SaveUserInCache(id string, user *User) {
	cache.GlobalUserCache.Add(id, user)
}

func GetUserByIDFromDB(id string) *User {
	cahceOrigin, ok := cache.GlobalUserCache.Get(id)
	if ok {
		ret, retOk := cahceOrigin.(*User)
		if retOk {
			return ret
		}
	}
	// load from db
	var ret User
	queryErr := db.GlobalDB.Where("id = ?", id).First(&ret).Error
	if queryErr != nil {
		if queryErr != gorm.ErrRecordNotFound {
			log.Errorf("GetUserByIDFromDB %s got error %s", id, queryErr.Error())
		}
		return nil
	}
	var dbEditor editor
	var dbContent LatestSessionContent
	contentErr := json.Unmarshal([]byte(ret.ContentJson), &dbContent)
	editorErr := json.Unmarshal([]byte(ret.EditorJson), &dbEditor)
	if contentErr != nil || editorErr != nil {
		log.Errorf("GetUserByIDFromDB %s unmarshal got contenterr %v,editorErr %v ", id, contentErr, editorErr)
	}
	ret.Editor = &dbEditor
	ret.LatestSessionContent = &dbContent
	cache.GlobalUserCache.Add(ret.ID, &ret)
	return &ret
}

// // Save saves the user's configurations in conf/users/{userID}.json.
// func (u *User) Save() bool {
// 	editorBytes, _ := json.Marshal(u.Editor)
// 	contentBytes, _ := json.Marshal(u.LatestSessionContent)
// 	u.EditorJson = string(editorBytes)
// 	u.ContentJson = string(contentBytes)

// 	bytes, err := json.MarshalIndent(u, "", "    ")

// 	if nil != err {
// 		log.Error(err)

// 		return false
// 	}

// 	if "" == string(bytes) {
// 		log.Error("Truncated user [" + u.ID + "]")

// 		return false
// 	}

// 	if err = ioutil.WriteFile(filepath.Join(Wide.Data, "users", u.ID+".json"),
// 		bytes, 0644); nil != err {
// 		log.Error(err)

// 		return false
// 	}

// 	return true
// }

// NewUser creates a user with the specified username and workspace.
func NewUser(id, name, avatar, workspace string) *User {
	now := time.Now().UnixNano()

	return &User{
		ID:                    id,
		Name:                  name,
		Avatar:                avatar,
		Workspace:             workspace,
		Locale:                Wide.Locale,
		GoFormat:              "gofmt",
		GoBuildArgsForLinux:   "-i",
		GoBuildArgsForWindows: "-i",
		GoBuildArgsForDarwin:  "-i",
		FontFamily:            "Helvetica",
		FontSize:              "14px",
		Theme:                 "dark",
		Keymap:                "wide",
		Created:               now,
		Updated:               now,
		Lived:                 now,
		Editor: &editor{
			FontFamily: "Consolas, 'Courier New', monospace",
			FontSize:   "14px",
			LineHeight: "14px",
			Theme:      "monokai",
			TabSize:    "4"},
	}
}

// WorkspacePath gets workspace path of the user.
//
// Compared to the use of Wide.Workspace, this function will be processed as follows:
//  1. Replace {WD} variable with the actual directory path
//  2. Replace ${GOPATH} with enviorment variable GOPATH
//  3. Replace "/" with "\\" (Windows)
func (u *User) WorkspacePath() string {
	w := u.Workspace
	w = strings.Replace(w, "${GOPATH}", os.Getenv("GOPATH"), 1)

	return filepath.FromSlash(w)
}

func (u *User) GetBuildArgs(executableFileName string) []string {
	goBuildArgs := make([]string, 0)
	goBuildArgs = append(goBuildArgs, "build", "-o", executableFileName, "-tags crypto")
	return goBuildArgs
}

// GetOwner gets the user the specified path belongs to. Returns "" if not found.
func GetOwner(path string) string {
	workSpacePrefix := filepath.Join(Wide.Data, "workspaces")
	if strings.HasPrefix(path, workSpacePrefix) {
		rel, _ := filepath.Rel(workSpacePrefix, path)
		sps := strings.SplitN(rel, string(os.PathSeparator), 2)
		if len(sps) > 0 {
			return strings.TrimSpace(sps[0])
		}
		return ""
	}
	return ""
	// for _, user := range Users {
	// 	if strings.HasPrefix(path, user.WorkspacePath()) {
	// 		return user.ID
	// 	}
	// }

}
